﻿namespace MSBuild.MKS.Tasks
{
    using System;
    using System.Diagnostics;
    using System.Text;
    using System.Xml;

    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;

    using MSBuild.MKS.Tasks.Exceptions;

    /// <summary>
    /// The mks commands.
    /// </summary>
    public class MksCommands
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MksCommands"/> class.
        /// </summary>
        public MksCommands()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MksCommands"/> class.
        /// </summary>
        /// <param name="taskID">
        /// The task id.
        /// </param>
        /// <param name="buildLabel">
        /// The build label.
        /// </param>
        /// <param name="logger">
        /// The logger.
        /// </param>
        public MksCommands(string taskID, string buildLabel, TaskLoggingHelper logger)
        {
            this.Log = logger;
            this.BuildLabel = buildLabel;
            this.TaskID = taskID;
        }

        /// <summary>
        /// Gets or sets BuildLabel.
        /// </summary>
        public string BuildLabel { get; set; }

        /// <summary>
        /// Gets or sets ChangePackageDescription.
        /// </summary>
        public string ChangePackageDescription { get; set; }

        /// <summary>
        /// Gets or sets ChangePackageID.
        /// </summary>
        public string ChangePackageID { get; set; }

        /// <summary>
        /// Gets or sets ChangePackageSummary.
        /// </summary>
        public string ChangePackageSummary { get; set; }

        /// <summary>
        /// Gets or sets CheckinComments.
        /// </summary>
        public string CheckinComments { get; set; }

        /// <summary>
        /// Gets or sets HostName.
        /// </summary>
        public string HostName { get; set; }

        /// <summary>
        /// Gets or sets the HostPort.
        /// </summary>
        public string HostPort { get; set; }

        /// <summary>
        /// Gets or sets Log.
        /// </summary>
        public TaskLoggingHelper Log { get; set; }

        /// <summary>
        /// Gets or sets Password.
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        /// Gets or sets TaskID.
        /// </summary>
        public string TaskID { get; set; }

        /// <summary>
        /// Gets or sets User.
        /// </summary>
        public string User { get; set; }

        /// <summary>
        /// The add.
        /// </summary>
        /// <param name="file">
        /// The file.
        /// </param>
        public void Add(string file)
        {
            Process proc = GetSiCommand();

            const string ArgsFormat = "add --retainWorkingFile --saveTimestamp --changePackageId={0} --description=\"{1}\" --xmlapi {2}";
            proc.StartInfo.Arguments = string.Format(ArgsFormat, this.ChangePackageID, this.CheckinComments, file);

            string output = RunProc(proc, ParseAddResult);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The checkin.
        /// </summary>
        /// <param name="file">
        /// The file.
        /// </param>
        public void Checkin(string file)
        {
            Process proc = GetSiCommand();

            const string ArgsFormat = "ci --nodefer --nobranch --changePackageId=\"{0}\" --checkinUnchanged --nocloseCP --description=\"{1}\" --xmlapi {2}";
            proc.StartInfo.Arguments = string.Format(ArgsFormat, this.ChangePackageID, this.CheckinComments, file);

            string output = RunProc(proc, ParseCheckinResult);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The checkout.
        /// </summary>
        /// <param name="file">
        /// The file.
        /// </param>
        public void Checkout(string file)
        {
            Process proc = GetSiCommand();

            const string ArgsFormat = "co --changePackageId=\"{0}\" --nomerge --nooverwritechanged --xmlapi {1}";
            proc.StartInfo.Arguments = string.Format(ArgsFormat, this.ChangePackageID, file);

            string output = RunProc(proc, ParseCheckoutResult);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The close change package.
        /// </summary>
        public void CloseChangePackage()
        {
            Process proc = GetSiCommand();

            const string ArgsFormat = "closecp --noconfirm --releaselocks --xmlapi {0}";
            proc.StartInfo.Arguments = string.Format(ArgsFormat, this.ChangePackageID);

            string output = RunProc(proc, ParseCloseChangePackageResult);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The connect.
        /// </summary>
        public void Connect()
        {
            Process proc = GetSiCommand();
            const string ArgsFormat = "connect --batch --hostname={0} --port={1} --user={2} --password={3}";
            proc.StartInfo.Arguments = string.Format(ArgsFormat, this.HostName, this.HostPort, this.User, this.Password);
            var output = RunProc(proc);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The disconnect command.
        /// </summary>
        public void Disconnect()
        {
            Process proc = GetSiCommand();
            const string ArgsFormat = "disconnect --batch";
            proc.StartInfo.Arguments = string.Format(ArgsFormat);
            var output = RunProc(proc);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The shutdown command.
        /// </summary>
        public void Exit()
        {
            Process proc = GetSiCommand();
            const string ArgsFormat = "exit --batch --shutdown --abort";
            proc.StartInfo.Arguments = string.Format(ArgsFormat);
            var output = RunProc(proc);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// The create change package.
        /// </summary>
        public void CreateChangePackage()
        {
            Process proc = GetSiCommand();

            const string ArgsFormat = "createcp --description=\"{0}\" --issueId={1} --summary=\"{2}\" --xmlapi";

            proc.StartInfo.Arguments = string.Format(
                ArgsFormat, this.ChangePackageDescription, this.TaskID, this.ChangePackageSummary);

            this.ChangePackageID = RunProc(proc, GetChangePackageId);
            this.Log.LogMessage(
                MessageImportance.Normal, string.Format("Create Change Package: {0}", this.ChangePackageID));
        }

        /// <summary>
        /// The resync.
        /// </summary>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        public void Resync(int timeout)
        {
            Process proc = GetSiCommand();

            const string Args = "resync -R --overwriteChanged --restoreTimestamp --forceConfirm=yes --includeDropped";
            proc.StartInfo.Arguments = Args;

            string output = RunProc(proc, timeout);
            this.Log.LogMessage(MessageImportance.Normal, output);
        }

        /// <summary>
        /// Gets the change package Id from the result xml.
        /// </summary>
        /// <param name="xmlResult">
        /// The xml result.
        /// </param>
        /// <returns>
        /// The get change package id.
        /// </returns>
        private static string GetChangePackageId(string xmlResult)
        {
            if (xmlResult == null)
            {
                throw new ArgumentNullException("xmlResult");
            }

            var doc = new XmlDocument();
            doc.LoadXml(xmlResult);

            var resultNode = doc.SelectSingleNode("//Field[@name='resultant']/Item/@id");

            if (resultNode != null)
            {
                var result = resultNode.Value;
                return result;
            }

            return "Unknown";
        }

        /// <summary>
        /// The get si command.
        /// </summary>
        /// <returns>
        /// The process object to use.
        /// </returns>
        private static Process GetSiCommand()
        {
            var proc = new Process
                {
                    StartInfo =
                        new ProcessStartInfo
                            {
                                UseShellExecute = false,
                                CreateNoWindow = true,
                                FileName = "si.exe",
                                RedirectStandardOutput = true
                            }
                };
            return proc;
        }

        /// <summary>
        /// Parses the xml result of the Add command.
        /// </summary>
        /// <param name="xml">
        /// The xml result.
        /// </param>
        /// <returns>
        /// The add result.
        /// </returns>
        private static string ParseAddResult(string xml)
        {
            // <Response command="add" app="si" version="4.10.0 4-1 5679">
            // <App-Connection port="7003" userID="djohnson" server="c1130.cvrx.com" /> 
            // - <WorkItems selectionType="INonMemberSelection">
            // - <WorkItem id="foo3.txt" context="c:/c1130-projects/MIS/project.pj" displayId="c:\c1130-projects\MIS\foo3.txt" modelType="si.NonMember">
            // -<Result>
            // - <Field name="resultant">
            // <Item id="foo3.txt" context="/Personal/DaveJohnson/Projects/MIS/project.pj" displayId="foo3.txt" modelType="si.Member" /> 
            // </Field>
            // </Result>
            // </WorkItem>
            // </WorkItems>
            // </Response>
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            var items = doc.SelectNodes("//WorkItem");
            var result = new StringBuilder();

            if (items != null)
            {
                foreach (XmlNode item in items)
                {
                    const string Proto = "Add Item: {0} added to {1}";
                    var resultNode = item.SelectSingleNode("Result/Field[@name='resultant']/Item/@context");
                    if (resultNode != null)
                    {
                        var mksMessage = resultNode.Value;
                        var resultIdNode = item.SelectSingleNode("@id");
                        if (resultIdNode != null)
                        {
                            var sandboxItem = resultIdNode.Value;
                            result.AppendLine(string.Format(Proto, sandboxItem, mksMessage));
                        }
                    }
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// Parses the checkin result xml.
        /// </summary>
        /// <param name="xml">
        /// The xml.
        /// </param>
        /// <returns>
        /// The parse checkin result.
        /// </returns>
        private static string ParseCheckinResult(string xml)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            var items = doc.SelectNodes("//WorkItem");
            var result = new StringBuilder();

            if (items != null)
            {
                foreach (XmlNode item in items)
                {
                    const string Proto = "Check In: {0}. MKS Response: {1}";
                    var mksMessageNode = item.SelectSingleNode("Result/Message/text()");
                    if (mksMessageNode != null)
                    {
                        var mksMessage = mksMessageNode.Value;
                        var sandboxItemNode = item.SelectSingleNode("@id");
                        if (sandboxItemNode != null)
                        {
                            var sandboxItem = sandboxItemNode.Value;
                            result.AppendLine(string.Format(Proto, sandboxItem, mksMessage));
                        }
                    }
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// Parses the checkout result xml.
        /// </summary>
        /// <param name="xml">
        /// The xml.
        /// </param>
        /// <returns>
        /// The parse checkout result.
        /// </returns>
        private static string ParseCheckoutResult(string xml)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            var items = doc.SelectNodes("//WorkItem");
            var result = new StringBuilder();

            if (items != null)
            {
                foreach (XmlNode item in items)
                {
                    const string Proto = "Check Out: {0}. MKS Response: {1}";
                    var mksMessageNode = item.SelectSingleNode("Result/Message/text()");
                    if (mksMessageNode != null)
                    {
                        var mksMessage = mksMessageNode.Value;
                        var sandboxItemNode = item.SelectSingleNode("@id");
                        if (sandboxItemNode != null)
                        {
                            var sandboxItem = sandboxItemNode.Value;
                            result.AppendLine(string.Format(Proto, sandboxItem, mksMessage));
                        }
                    }
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// Parses close change package result.
        /// </summary>
        /// <param name="xml">
        /// The xml.
        /// </param>
        /// <returns>
        /// The close change package result.
        /// </returns>
        private static string ParseCloseChangePackageResult(string xml)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            var items = doc.SelectNodes("//WorkItem");
            var result = new StringBuilder();

            if (items != null)
            {
                foreach (XmlNode item in items)
                {
                    const string Proto = "Close Change Package: {0}. MKS Response: {1}";
                    var mksMessageNode = item.SelectSingleNode("Result/Message/text()");
                    if (mksMessageNode != null)
                    {
                        var mksMessage = mksMessageNode.Value;
                        var sandboxItemNode = item.SelectSingleNode("@id");
                        if (sandboxItemNode != null)
                        {
                            var sandboxItem = sandboxItemNode.Value;
                            result.AppendLine(string.Format(Proto, sandboxItem, mksMessage));
                        }
                    }
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// Runs the process with a specified timeout.
        /// </summary>
        /// <param name="proc">
        /// The process instance.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        /// <returns>
        /// Thrown if the task times out.
        /// </returns>
        private static string RunProc(Process proc, int timeout = 10000)
        {
            proc.Start();
            var exited = proc.WaitForExit(timeout);

            if (!exited)
            {
                throw new TaskTimeoutException(
                    string.Format(
                        "Task Timed Out. Task Info: {0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments));
            }

            var output = proc.StandardOutput.ReadToEnd();
            return output;
        }

        /// <summary>
        /// Runs a proces with a callback to a result handler.
        /// </summary>
        /// <param name="proc">
        /// The process object.
        /// </param>
        /// <param name="resultHandler">
        /// The result handler delegate.
        /// </param>
        /// <returns>
        /// The stdout string.
        /// </returns>
        private static string RunProc(Process proc, Func<string, string> resultHandler)
        {
            var output = RunProc(proc);
            try
            {
                var result = resultHandler(output);
                return result;
            }
            catch (Exception)
            {
                throw new MksCommandResponseParseException(
                    string.Format(
                        "Task Result Parsing Exception. Task Info: {0} {1}", 
                        proc.StartInfo.FileName, 
                        proc.StartInfo.Arguments));
            }
        }
    }
}